/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { BaseType, SRClass, SRField, TypeDecorator, ExposeDecorator, ExcludeDecorator, SRDiscriminator, SRPassThrough, UnclearDecorator} from './SRScene';
import { template, Type } from './fieldTemplates';
import { classFactory, enumHelper, T } from './nameUtils';

function aliasOf(field: SRField): string | null {
    for (let decorator of field.getDecorators()) {
        if (decorator instanceof ExposeDecorator) {
            return decorator.getName()?.getName()?? null;
        }
    }
    return null;
}

function genClassDeclaration(cls: SRClass): string {
    return `${cls.getModifiers().includes('abstract')? 'abstract ': ''}class ${cls.getName()}${cls.getSuperClass()? ' extends ' + cls.getSuperClass().getName(): ''} implements ISerializable<${cls.getName()}>`;
}

function getConstructorOfClass(cls: SRClass): string {
    const fieldStmt: string = cls.getFields().map(field => {
        if (needInitByCtor(field)) {
            return genCtorInitialization(field);
        }
    }).filter(it => it != null).join(`;\n${T}${T}`);
    if (cls.getModifiers().includes('abstract')) {
        return '';
    } else {
        return `constructor() {
${T}${T}${cls.getSuperClass() ? 'super();\n': ''}${fieldStmt}
${T}}`;
    }
}

export function genClass(cls: SRClass): string {
        return `@Sendable
export ${genClassDeclaration(cls)} {
${T}${cls.getFields().map(genFieldDeclaration).join(`\n${T}`)}

${T}${getConstructorOfClass(cls)}

${T}${cls.getMethods().filter(m => m.getName() != 'constructor').map(m => m.getOriginalCode()).join('\n' + T)}
    
${T}mergeFrom(jsonObj: object): void {
${T}${T}${genMergeFromStmts(cls)}
${T}}
${T}toJson(): object {
${T}${T}let ans:object = ${cls.getSuperClass()? 'super.toJson();': 'new Object;'}
${T}${T}${cls.getFields().map(field=>genToJsonStmt(field, "ans")).join('\n' + T.repeat(2))}
${T}${T}return ans;
${T}}
}
`;
}

export function genClassFactory(cls: SRClass): string {
    if (cls.getModifiers().includes('abstract')) {
        return '';
    }
    const factory = classFactory(cls.getName());
    return `
@Sendable
export class ${factory} implements ISerializableType<${cls.getName()}> {
${T}fromJson(obj: object): ${cls.getName()} {
${T}${T}const o = new ${cls.getName()}();
${T}${T}o.mergeFrom(obj);
${T}${T}return o;
${T}}

${T}toJson(o: ${cls.getName()}): object {
${T}${T}return o.toJson();
${T}}

${T}static instance: ${factory} = new ${factory}();
}`;
}

export function genFieldDeclaration(field: SRField): string {
    const fieldType = {...field.getType(), optional:isOptional(field)};
    const convertType = template(fieldType, 'convertType');
    const initExpr: string | null = field.getInitializer();
    const optionalExpr: string = isOptional(field)? '?' : '';
    return `${field.getName()}${optionalExpr}: ${convertType(fieldType)}${initExpr ? (' = ' + initExpr) : ''};`;
}

export function genCtorInitialization(field: SRField): string | null {
    const fieldType = {...field.getType(), optional:isOptional(field)};
    const ctor = template(fieldType, 'ctor')(fieldType);
    return ctor ? `this.${field.getName()} = new ${ctor}()` : null;
}

export function genMergeFromStmts(cls:SRClass):string {
    const passThroughField:SRField[] = cls.getFields().filter(isPassThrough);
    let passThroughStmt:string = '';
    if(passThroughField.length > 0) {
        passThroughStmt = `if(jsonObj){
${passThroughField.map(f=>T.repeat(3) + genMergeFromStmt(f, 'jsonObj')).join('\n')}
${T}${T}}`
    }
    const postProcessStmt = isPostProcess(cls)? 'this.postProcess();':'';
    const superClassStmt = cls.getSuperClass()? 'super.mergeFrom(jsonObj)\n': '';
    return `${superClassStmt}${passThroughStmt}${cls.getFields().filter(f=>!isPassThrough(f)).map(f => {
            return genMergeFromStmt(f, 'jsonObj')
        }).join(`\n${T}`)}
${T}${T}${postProcessStmt}`
}

export function genMergeFromStmt(field: SRField, param: string): string {
    if (field.getDecorators().find(decorator => decorator instanceof ExcludeDecorator)) {
        return '';
    }

    const fieldType = field.getType();
    const alias = aliasOf(field);
    const type: Type = {...fieldType, optional: isOptional(field)};
    const decorator: TypeDecorator = field.getDecorators().find(dec => dec instanceof TypeDecorator) as TypeDecorator;
    if (decorator?.getDiscriminator() instanceof SRDiscriminator) {
        type.discriminator = decorator.getDiscriminator() as SRDiscriminator;
    }
    const merge = template(type, 'copy')(type);
    if (isPassThrough(field)) {
        return `${merge(param, `this.${field.getName()}`)}`;
    }
    const mergeStmt = `${merge(`${alias ? `${param}['${alias}'] ?? ` : ''}${param}['${field.getName()}']`, `this.${field.getName()}`)}`; 
    return `
${T}${T}if (${alias ? `${param}['${alias}'] || ` : ''}${param}['${field.getName()}']) {
${T}${T}${T}${mergeStmt}
${T}${T}}`;
}

export function genToJsonStmt(field: SRField, param: string): string {
    if (field.getDecorators().find(decorator => decorator instanceof ExcludeDecorator)) {
        return '';
    }
    // todo: support passThrough

    const fieldType = field.getType();
    const alias = aliasOf(field);
    const type: Type = {...fieldType, optional: isOptional(field)};
    const decorator: TypeDecorator = field.getDecorators().find(dec => dec instanceof TypeDecorator) as TypeDecorator;
    if (decorator?.getDiscriminator() instanceof SRDiscriminator) {
        type.discriminator = decorator.getDiscriminator() as SRDiscriminator;
    }
    const toJson = template(type, 'toJson')(type);
    if (needJudgmentInToJson(field)) {
        return `
${T}${T}if(this.${field.getName()}) {
${T}${T}${T}${toJson(`${field.getName()}`, `${param}['${alias ?? field.getName()}']`)}
${T}${T}}`
    }
    return toJson(`${field.getName()}`, `${param}['${alias ?? field.getName()}']`)
}

export function genEnumHelper(cls: SRClass): string {
    const helper = enumHelper(cls.getName());
    const numberTypeExpr = cls.getFields().find(f=>f.getType().category === BaseType.NUMBER);
    return `
${cls.getModifiers().join(" ")} enum ${cls.getName()} {
${T}${cls.getFields().map(field=>{
            const initExpr: string | null = field.getInitializer();
            return `${field.getName()}${initExpr ? (' = ' + initExpr) : ''}`
        }).join(",\n" + T)}
}
@Sendable
export class ${helper} {
${T}static toEnum(obj: object): ${cls.getName()} {
${T}${T}if (typeof obj === 'string') {
${T}${T}${T}switch (String(obj)) {
${T}${T}${T}${cls.getFields().map(field=> {
                            return `
${T}${T}${T}${T}case '${field.getName()}':{
${T}${T}${T}${T}${T}return ${cls.getName()}.${field.getName()};
${T}${T}${T}${T}}`}).join("\n")}
${T}${T}${T}}
${T}${T}}${numberTypeExpr!==undefined?` else if (typeof obj === 'number') {
${T}${T}${T}return Number(obj);
${T}${T}}`:``} 
${T}${T}throw new Error('Invalid ${cls.getName()}: ' + JSON.stringify(obj));
${T}}
}`;
}

export function copyMethods(cls: SRClass): string {
    return '';
}

export function isPassThrough(field: SRField):boolean {
    const decorator = field.getDecorators().find(decorator=> {
        if (decorator instanceof TypeDecorator) {
            return decorator.getDiscriminator() instanceof SRPassThrough;
        }
    })
    return decorator!==undefined;
}

export function isPostProcess(cls: SRClass):boolean {
    const decorator = cls.getDecorators().find(decorator=> {
        if (decorator instanceof UnclearDecorator) {
            return decorator.getType() ==='PostProcess'
        }
    })
    return decorator!==undefined;
}

/**
 * if not initialized, need to add an if judgment in toJson stmt
 * @param field field
 * @returns true if need judgment in toJson stmt
 */
export function needJudgmentInToJson(field: SRField): boolean {
    if (isDefaultOptional(field) && field.getType().category!==BaseType.CLASS && field.getType().category!==BaseType.OTHERS) {
        return false;
    }
    return field.getInitializer().length === 0 && isOptional(field);
}

/**
 * whether need to be initialized in constructor
 * @param field field
 * @returns return true if need to be initialized in constructor
 */
export function needInitByCtor(field: SRField): boolean {
    if (field.getInitializer().length !== 0) {
        return false;
    }
    return !field.isOptional() && !isDefaultOptional(field);
}

export function isOptional(field: SRField): boolean {
    if (field.isOptional()) {
        return true;
    }
    // default optional field type && not initialized is optional
    return isDefaultOptional(field) && field.getInitializer().length === 0;
}

export function isDefaultOptional(field: SRField): boolean {
    switch(field.getType().category) {
        case BaseType.BOOLEAN:
        case BaseType.NUMBER:
        case BaseType.BIGINIT:
        case BaseType.STRING:
        case BaseType.OBJECT:
        case BaseType.CLASS:
        case BaseType.OTHERS:
        case BaseType.ENUM:
        case BaseType.UNION:
            return true;
        case BaseType.ARRAY:
        case BaseType.COLLECTIONS_ARRAY:
        case BaseType.COLLECTIONS_SET:
        case BaseType.COLLECTIONS_MAP:
        case BaseType.SET:
        case BaseType.MAP:
            return false;
    }
}